use std::{
    future::Future,
    rc::Rc,
    sync::{Arc, Mutex},
};

use reflexo::hash::Fingerprint;
use reflexo::vector::ir::{self, Module, Page, Point, Scalar, Size, TextItem, TransformItem};
use reflexo::{error::prelude::*, ImmutStr};
use reflexo_vec2canvas::{CanvasElem, CanvasNode, CanvasOp, CanvasStateGuard};
use web_sys::{
    js_sys::Reflect,
    wasm_bindgen::{JsCast, JsValue},
    window, Element, HtmlCanvasElement, HtmlDivElement, HtmlElement, SvgGraphicsElement,
    SvgsvgElement,
};

use crate::{
    canvas_backend::CanvasBackend, factory::XmlFactory, svg_backend::FETCH_BBOX_TIMES, DomContext,
};

pub struct DomPage {
    /// Index
    idx: usize,
    /// The element to track.
    elem: HtmlElement,
    /// The canvas element to track.
    canvas: HtmlCanvasElement,
    /// The svg element to track.
    svg: SvgsvgElement,
    /// The semantics element to track.
    semantics: HtmlDivElement,
    /// The layout data, currently there is only a page in layout.
    layout_data: Option<Page>,
    /// The next page data
    dirty_layout: Option<Page>,
    /// The viewport.
    viewport: ir::Rect,
    /// The BBox of the page.
    bbox: ir::Rect,
    /// The realized element.
    pub realized: Rc<Mutex<Option<TypstPageElem>>>,
    /// The realized canvas element.
    realized_canvas: Option<CanvasNode>,
    /// The flushed semantics state.
    semantics_state: Option<(Page, bool)>,
    /// The flushed canvas state.
    canvas_state: Rc<Mutex<Option<CanvasRenderState>>>,
    /// Whether the page is visible.
    is_visible: bool,
    /// The group element.
    g: Element,
    /// The stub element.
    stub: Element,
}

impl Drop for DomPage {
    fn drop(&mut self) {
        self.elem.remove();
    }
}

struct CanvasRenderState {
    // (Page, f32)
    rendered: Page,
    ppp: f32,
    render_entire_page: bool,
}

impl DomPage {
    pub fn new_at(elem: HtmlElement, tmpl: XmlFactory, idx: usize) -> Self {
        // https://stackoverflow.com/questions/20242806/hole-in-overlay-with-css
        const TEMPLATE: &str = r#"<div class="typst-dom-page"><canvas class="typst-back-canvas" style="--reflexo-clip-lo-x: 0px; --reflexo-clip-lo-y: 0px; --reflexo-clip-hi-x: 0px; --reflexo-clip-hi-y: 0px; clip-path: polygon( evenodd, 0 0, 100% 0, 100% 100%, 0% 100%, 0 0, var(--reflexo-clip-lo-x) var(--reflexo-clip-lo-y), var(--reflexo-clip-hi-x) var(--reflexo-clip-lo-y), var(--reflexo-clip-hi-x) var(--reflexo-clip-hi-y), var(--reflexo-clip-lo-x) var(--reflexo-clip-hi-y), var(--reflexo-clip-lo-x) var(--reflexo-clip-lo-y))"></canvas><svg class="typst-svg-page" viewBox="0 0 0 0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<g></g><stub></stub></svg><div class="typst-html-semantics"><div/></div>"#;

        let me = tmpl.create_element(TEMPLATE);
        me.set_attribute("data-index", &idx.to_string()).unwrap();
        let canvas: HtmlCanvasElement = me.first_element_child().unwrap().dyn_into().unwrap();
        let svg: SvgsvgElement = canvas.next_element_sibling().unwrap().dyn_into().unwrap();
        let semantics: HtmlDivElement = svg.next_element_sibling().unwrap().dyn_into().unwrap();
        let g = svg.first_element_child().unwrap();
        let stub = g.next_element_sibling().unwrap();
        g.remove();

        // window.bindSemantics
        let bind_semantics_handler = window().unwrap();
        let bind_semantics_handler =
            Reflect::get(&bind_semantics_handler, &"typstBindSemantics".into())
                .unwrap()
                .dyn_into::<web_sys::js_sys::Function>()
                .unwrap();
        bind_semantics_handler
            .call3(&JsValue::UNDEFINED, &me, &svg, &semantics)
            .unwrap();

        // for debug canvas
        // svg.style().set_property("visibility", "hidden").unwrap();
        // for debug svg
        // canvas.style().set_property("visibility", "hidden").unwrap();

        elem.append_child(&me).unwrap();

        // todo: default or none?
        let viewport = ir::Rect::default();
        let bbox = viewport;
        Self {
            is_visible: false,
            g,
            stub,
            idx,
            elem: me.dyn_into().unwrap(),
            canvas,
            svg,
            semantics,
            viewport,
            bbox,
            layout_data: None,
            dirty_layout: None,
            realized: Rc::new(Mutex::new(None)),
            realized_canvas: None,
            canvas_state: Rc::new(Mutex::new(None)),
            semantics_state: None,
        }
    }

    pub fn track_data(&mut self, data: &Page) -> bool {
        if self.layout_data.as_ref() == Some(data) {
            return false;
        }

        self.dirty_layout = Some(data.clone());
        true
    }

    fn pull_viewport(&mut self, viewport: Option<tiny_skia::Rect>) {
        self.viewport = viewport
            .and_then(|viewport| {
                tiny_skia::Rect::from_ltrb(
                    self.bbox.lo.x.0 - 1.,
                    viewport.top(),
                    self.bbox.hi.x.0 + 1.,
                    viewport.bottom(),
                )
            })
            .map(From::from)
            .unwrap_or(self.bbox);
        #[cfg(feature = "debug_repaint")]
        web_sys::console::log_2(
            &format!(
                "pull_viewport {idx} vp:{vp:?}, bbox {bbox:?}",
                idx = self.idx,
                vp = viewport,
                bbox = self.bbox,
            )
            .into(),
            &self.elem,
        );
    }

    fn change_svg_visibility(&mut self, should_visible: bool) {
        if should_visible != self.is_visible {
            web_sys::console::log_2(
                &format!(
                    "change_svg_visibility {idx} {should_visible}",
                    idx = self.idx,
                    should_visible = should_visible
                )
                .into(),
                &self.elem,
            );
            self.is_visible = should_visible;
            if should_visible {
                self.stub.replace_with_with_node_1(&self.g).unwrap();
            } else {
                self.g.replace_with_with_node_1(&self.stub).unwrap();
            }
        }
    }

    pub fn relayout(&mut self, ctx: &CanvasBackend) -> Result<()> {
        if let Some(data) = self.dirty_layout.take() {
            self.do_relayout(ctx, data)?
        }

        Ok(())
    }

    fn do_relayout(&mut self, ctx: &CanvasBackend, data: Page) -> Result<()> {
        #[cfg(feature = "debug_relayout")]
        web_sys::console::log_2(
            &format!("re-layout {idx} {data:?}", idx = self.idx).into(),
            &self.elem,
        );

        let prev_size = self.layout_data.as_ref().map(|d| d.size);

        if prev_size.map(|s| s != data.size).unwrap_or(true) {
            web_sys::console::log_2(
                &format!("resize {idx} {data:?}", idx = self.idx).into(),
                &self.elem,
            );

            // calculate the width and height of the svg
            // todo: don't update if individual not changed
            let w = data.size.x.0;
            let h = data.size.y.0;

            self.elem
                .set_attribute("data-width", &w.to_string())
                .unwrap();
            self.elem
                .set_attribute("data-height", &h.to_string())
                .unwrap();
            let style = self.elem.style();
            style
                .set_property("--data-page-width", &format!("{w:.3}px"))
                .unwrap();
            style
                .set_property("--data-page-height", &format!("{h:.3}px"))
                .unwrap();
            self.svg
                .set_attribute("viewBox", &format!("0 0 {w} {h}"))
                .unwrap();

            self.svg
                .set_attribute("data-width", &w.to_string())
                .unwrap();
            self.svg
                .set_attribute("data-height", &h.to_string())
                .unwrap();
            self.bbox = ir::Rect {
                lo: Point::default(),
                hi: data.size,
            };

            let ppp = ctx.pixel_per_pt;
            self.canvas.set_width((w * ppp) as u32);
            self.canvas.set_height((h * ppp) as u32);
            *self.canvas_state.lock().unwrap() = None;
            style
                .set_property(
                    "--data-canvas-scale",
                    &format!("{:.3}", 1. / ctx.pixel_per_pt),
                )
                .unwrap();
        }

        // todo: cache
        let prev_data = self.layout_data.clone();
        if prev_data.map(|d| d != data).unwrap_or(true) {
            #[cfg(feature = "debug_relayout")]
            web_sys::console::log_2(
                &format!("dirty layout detected {idx}", idx = self.idx).into(),
                &self.elem,
            );
            // self.change_svg_visibility(false);
            *self.realized.lock().unwrap() = None;
            *self.canvas_state.lock().unwrap() = None;
            self.semantics_state = None;
            self.realized_canvas = None;
            self.layout_data = Some(data);
        }

        Ok(())
    }

    pub fn need_repaint_svg(&mut self, viewport: Option<tiny_skia::Rect>) -> bool {
        self.pull_viewport(viewport);

        let should_visible = !self.bbox.intersect(&self.viewport).is_empty();

        if cfg!(feature = "debug_repaint_svg") {
            web_sys::console::log_1(
                &format!(
                    "need_repaint_svg({should_visible}) bbox:{bbox:?} view:{viewport:?}",
                    bbox = self.bbox,
                    viewport = self.viewport,
                )
                .into(),
            );
        }

        self.change_svg_visibility(should_visible);
        should_visible
    }

    pub fn repaint_svg(&mut self, ctx: &mut DomContext<'_, '_>) -> Result<()> {
        let should_visible = !self.bbox.intersect(&self.viewport).is_empty();

        if cfg!(feature = "debug_repaint") {
            web_sys::console::log_1(
                &format!(
                    "repaint_root({should_visible}) bbox:{bbox:?} view:{viewport:?}",
                    bbox = self.bbox,
                    viewport = self.viewport,
                )
                .into(),
            );
        }

        self.change_svg_visibility(should_visible);
        if !self.is_visible {
            return Ok(());
        }

        let mut realized = self.realized.lock().unwrap();

        if realized.is_none() {
            let data = self.layout_data.clone().unwrap();

            // todo incremental
            if self.realized_canvas.is_none() {
                self.realized_canvas = Some(ctx.canvas_backend.render_page(ctx.module, &data)?);
            }

            // Realize svg
            let g = ctx.svg_backend.render_page(ctx.module, &data, &self.g);
            let mut elem = TypstPageElem::from_elem(ctx, g, data);
            if elem.g.canvas.is_none() {
                elem.g.attach_canvas(self.realized_canvas.clone().unwrap());
            }

            *realized = Some(elem);

            // window.bindSvgDom
            let bind_dom_handler = window().unwrap();
            let bind_dom_handler = Reflect::get(&bind_dom_handler, &"typstBindSvgDom".into())
                .unwrap()
                .dyn_into::<web_sys::js_sys::Function>()
                .unwrap();
            bind_dom_handler
                .call2(&JsValue::UNDEFINED, &self.elem, &self.svg)
                .unwrap();
        }

        let ts = tiny_skia::Transform::identity();

        if let Some(attached) = realized.as_mut() {
            if cfg!(feature = "debug_repaint_svg") {
                web_sys::console::log_2(
                    &format!(
                        "repaint_start {idx} {vp:?}, bbox_query {fetch_times:?}",
                        idx = self.idx,
                        vp = self.viewport,
                        fetch_times = FETCH_BBOX_TIMES.load(std::sync::atomic::Ordering::SeqCst)
                    )
                    .into(),
                    &self.elem,
                );
            }

            attached.repaint_svg(ctx, ts, self.viewport);

            if cfg!(feature = "debug_repaint_svg") {
                web_sys::console::log_2(
                    &format!(
                        "repaint_end {idx} {vp:?}, bbox_query {fetch_times:?}",
                        idx = self.idx,
                        vp = self.viewport,
                        fetch_times = FETCH_BBOX_TIMES.load(std::sync::atomic::Ordering::SeqCst)
                    )
                    .into(),
                    &self.elem,
                );
            }
        }
        Ok(())
    }

    pub fn need_repaint_semantics(&mut self) -> bool {
        self.semantics_state.as_ref().is_none_or(|e| {
            let (data, layout_heavy) = e;
            e.0.content != data.content || (self.is_visible && !*layout_heavy)
        })
    }

    pub fn repaint_semantics(&mut self, ctx: &mut DomContext<'_, '_>) -> Result<()> {
        let init_semantics = self.semantics_state.as_ref().is_none_or(|e| {
            let (data, _layout_heavy) = e;
            e.0.content != data.content
        });

        if init_semantics {
            let do_heavy = self.is_visible;

            let data = self.layout_data.clone().unwrap();
            #[cfg(feature = "debug_repaint_semantics")]
            web_sys::console::log_1(
                &format!("init semantics({do_heavy}): {} {:?}", self.idx, data).into(),
            );

            let output = ctx.semantics_backend.render(ctx.module, &data, do_heavy);
            self.semantics.set_inner_html(&output);
            self.semantics_state = Some((data, do_heavy));
        }

        if !self.is_visible {
            return Ok(());
        }

        if self.semantics_state.as_ref().is_none_or(|e| e.1) {
            return Ok(());
        }

        let data = self.layout_data.clone().unwrap();
        web_sys::console::log_1(&format!("layout heavy semantics: {} {:?}", self.idx, data).into());

        let output = ctx.semantics_backend.render(ctx.module, &data, true);
        self.semantics.set_inner_html(&output);

        self.semantics_state.as_mut().unwrap().1 = true;
        Ok(())
    }

    pub fn need_prepare_canvas(&mut self, module: &Module, b: &mut CanvasBackend) -> Result<bool> {
        // already pulled
        // self.pull_viewport(viewport);

        let need_repaint = self.need_repaint_canvas(b);

        if need_repaint {
            let data = self.layout_data.clone().unwrap();

            // todo incremental
            if self.realized_canvas.is_none() {
                self.realized_canvas = Some(b.render_page(module, &data)?);
            }

            let ppp = b.pixel_per_pt;
            let ts = tiny_skia::Transform::from_scale(ppp, ppp);
            let should_load_resource = self.realized_canvas.as_ref().unwrap().prepare(ts).is_some();

            #[cfg(feature = "debug_repaint_canvas")]
            web_sys::console::log_1(
                &format!(
                    "canvas state prepare({}), should_load_resource:{}",
                    self.idx, should_load_resource
                )
                .into(),
            );

            return Ok(should_load_resource);
        }

        Ok(false)
    }

    pub fn prepare_canvas(
        &mut self,
        ctx: &mut DomContext<'_, '_>,
    ) -> Result<Option<Arc<CanvasElem>>> {
        let need_repaint = self.need_repaint_canvas(ctx.canvas_backend);

        let res = if need_repaint {
            let data = self.layout_data.clone().unwrap();

            // todo incremental
            if self.realized_canvas.is_none() {
                self.realized_canvas = Some(ctx.canvas_backend.render_page(ctx.module, &data)?);
            }

            Some(self.realized_canvas.clone().unwrap())
        } else {
            None
        };

        Ok(res)
    }

    pub fn need_repaint_canvas(&mut self, b: &CanvasBackend) -> bool {
        // already pulled
        // self.pull_viewport(viewport);

        if self.is_visible {
            return true;
        }

        let state = self.layout_data.as_ref().unwrap();
        self.canvas_state.lock().unwrap().as_ref().is_none_or(|s| {
            !s.render_entire_page || s.rendered != *state || s.ppp != b.pixel_per_pt
        })
    }

    pub fn repaint_canvas(
        &mut self,
        viewport: Option<tiny_skia::Rect>,
        ppp: f32,
    ) -> Result<impl Future<Output = ()>> {
        let render_entire_page = self.realized.lock().unwrap().is_none() || !self.is_visible;

        if let Some(attached) = self.realized.lock().unwrap().as_mut() {
            if attached.g.canvas.is_none() {
                // todo incremental
                attached.attach_canvas(self.realized_canvas.clone().unwrap());
            }
        };

        let canvas = self.canvas.clone();

        let idx = self.idx;
        let state = self.layout_data.clone().unwrap();
        let canvas_state = self.canvas_state.clone();
        let canvas_elem = self.realized_canvas.clone().unwrap();
        let elem = self.realized.clone();

        web_sys::console::log_1(
            &format!("canvas check: {} {}", idx, elem.lock().unwrap().is_some()).into(),
        );

        #[allow(clippy::await_holding_lock)]
        return Ok(async move {
            let canvas_ctx = canvas
                .get_context("2d")
                .unwrap()
                .unwrap()
                .dyn_into::<web_sys::CanvasRenderingContext2d>()
                .unwrap();

            let mut elem = elem.lock().unwrap();

            web_sys::console::log_1(&format!("canvas render: {idx} {viewport:?}").into());

            'render_canvas: {
                let _global_guard = CanvasStateGuard::new(&canvas_ctx);

                if canvas_state
                    .lock()
                    .unwrap()
                    .as_ref()
                    .is_some_and(|s| s.rendered == state && s.ppp == ppp)
                {
                    break 'render_canvas;
                }
                #[cfg(feature = "debug_repaint_canvas")]
                web_sys::console::log_1(
                    &format!(
                        "canvas state changed, render all: {} {}",
                        idx,
                        elem.is_none()
                    )
                    .into(),
                );

                // todo: memorize canvas fill
                canvas_ctx.clear_rect(
                    0.,
                    0.,
                    (state.size.x.0 * ppp) as f64,
                    (state.size.y.0 * ppp) as f64,
                );

                *canvas_state.lock().unwrap() = Some(CanvasRenderState {
                    rendered: state.clone(),
                    ppp,
                    render_entire_page: true,
                });

                let ts = tiny_skia::Transform::from_scale(ppp, ppp);
                canvas_elem.realize(ts, &canvas_ctx).await;
            }

            let mut clip_rect = None;
            'clip_canvas: {
                if render_entire_page {
                    break 'clip_canvas;
                } else {
                    let all_painted = canvas_state
                        .clone()
                        .lock()
                        .unwrap()
                        .as_ref()
                        .is_some_and(|e| e.render_entire_page);

                    let Some(elem) = elem.as_mut() else {
                        panic!("realized is none for partial canvas render");
                    };

                    if all_painted {
                        elem.mark_painted();
                    }

                    let ts = tiny_skia::Transform::identity();
                    let Some(damage_rect) = elem.get_damage_rect(ts) else {
                        break 'clip_canvas;
                    };

                    #[cfg(feature = "debug_repaint_canvas")]
                    web_sys::console::log_1(
                        &format!("canvas partial render: {idx} {damage_rect:?}").into(),
                    );

                    // let x = damage_rect.lo.x.0;
                    let y = damage_rect.lo.y.0;
                    // let w = damage_rect.width().0;
                    let h = damage_rect.height().0;

                    let data_size = state.size;
                    // let dx = data_size.x.0.max(1.);
                    let dy = data_size.y.0.max(1.);

                    let x_ratio = 0.;
                    let y_ratio = y / dy * 100.;
                    let w_ratio = 100.;
                    let h_ratio = h / dy * 100.;

                    clip_rect = Some((x_ratio, y_ratio, w_ratio, h_ratio));

                    let _ = DomPage::repaint_canvas;
                }
            }

            let clip_key = format!("{clip_rect:?}");

            // data-clip-rect-state
            const CLIP_KEY: &str = "data-clip-rect-state";
            let clip_state = canvas.get_attribute(CLIP_KEY).unwrap_or_default();

            if clip_state != clip_key {
                let _ = canvas.set_attribute(CLIP_KEY, &clip_key);

                if let Some((x, y, w, h)) = clip_rect {
                    let modify = |p, x| canvas.style().set_property(p, &format!("{x:.3}%"));
                    let _ = modify("--reflexo-clip-lo-x", x);
                    let _ = modify("--reflexo-clip-lo-y", y);
                    let _ = modify("--reflexo-clip-hi-x", x + w);
                    let _ = modify("--reflexo-clip-hi-y", y + h);
                } else {
                    let modify = |p| canvas.style().remove_property(p);
                    let _ = modify("--reflexo-clip-lo-x");
                    let _ = modify("--reflexo-clip-lo-y");
                    let _ = modify("--reflexo-clip-hi-x");
                    let _ = modify("--reflexo-clip-hi-y");
                }
            }
        });
    }
}

#[derive(Debug)]
pub struct TypstPageElem {
    pub stub: Element,
    pub g: TypstElem,
    pub clip_paths: Element,
    pub style_defs: Element,
}

impl TypstPageElem {
    pub fn from_elem(ctx: &mut DomContext<'_, '_>, g: Element, data: Page) -> Self {
        let g = g.first_element_child().unwrap();
        let stub = ctx.create_stub();
        let clip_paths = g.next_element_sibling().unwrap();
        let style_defs = clip_paths.next_element_sibling().unwrap();
        let attached = Self::attach_html(ctx, g.clone().dyn_into().unwrap(), data.content);

        Self {
            g: attached,
            stub,
            clip_paths,
            style_defs,
        }
    }
}

#[derive(Debug)]
pub enum TypstDomExtra {
    Group(GroupElem),
    Item(TransformElem),
    Label(LabelElem),
    Image(ImageElem),
    Text(TextElem),
    Path(PathElem),
    RawHtml(HtmlElem),
    Link(LinkElem),
    ContentHint(ContentHintElem),
}

#[derive(Debug)]
pub struct TypstElem {
    pub is_svg_visible: bool,
    pub is_canvas_painted: bool,
    pub f: Fingerprint,
    pub extra: TypstDomExtra,

    /// Stub backend specific data
    pub stub: Element,
    /// Svg backend specific data
    pub g: SvgGraphicsElement,
    /// Canvas backend specific data
    pub canvas: Option<CanvasNode>,
}

#[derive(Debug)]
pub struct ImageElem {
    pub size: Size,
}

#[derive(Debug)]
pub struct TextElem {
    pub upem: Scalar,
    pub meta: TextItem,
}
#[derive(Debug)]
pub struct HtmlElem {}
#[derive(Debug)]
pub struct PathElem {}
#[derive(Debug)]
pub struct LinkElem {}
#[derive(Debug)]
pub struct ContentHintElem {
    pub hint: char,
}

#[derive(Debug)]
pub struct TransformElem {
    pub trans: TransformItem,
    pub child: Box<TypstElem>,
}

#[derive(Debug)]
pub struct LabelElem {
    pub label: ImmutStr,
    pub child: Box<TypstElem>,
}

#[derive(Debug)]
pub struct GroupElem {
    pub children: Vec<(Point, TypstElem)>,
}
